﻿// ------------------------------------------------------------------------
//  Copyright 2025 The Dapr Authors
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//      http://www.apache.org/licenses/LICENSE-2.0
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//  ------------------------------------------------------------------------

using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Dapr.Common;
using Dapr.Cryptography.Extensions;
using Dapr.Common.Extensions;
using Dapr.Cryptography.Encryption.Models;
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;

namespace Dapr.Cryptography.Encryption;

/// <summary>
/// A client for performing cryptography operations with Dapr.
/// </summary>
[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")]
internal sealed class DaprEncryptionGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprEncryptionClient(client, httpClient, daprApiToken: daprApiToken)
{
    /// <summary>
    /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality.
    /// </summary>
    /// <param name="vaultResourceName">The name of the vault resource used by the operation.</param>
    /// <param name="plaintextBytes">The bytes of the plaintext value to encrypt.</param>
    /// <param name="keyName">The name of the key to use from the Vault for the encryption operation.</param>
    /// <param name="encryptionOptions">Options informing how the encryption operation should be configured.</param>
    /// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
    /// <returns>An array of encrypted bytes.</returns>
    public override async Task<ReadOnlyMemory<byte>> EncryptAsync(
        string vaultResourceName,
        ReadOnlyMemory<byte> plaintextBytes,
        string keyName,
        EncryptionOptions encryptionOptions,
        CancellationToken cancellationToken = default)
    {
        using var memoryStream = plaintextBytes.CreateMemoryStream(true);
        var encryptionResult = EncryptAsync(vaultResourceName, memoryStream, keyName, encryptionOptions, cancellationToken);

        var bufferedResult = new ArrayBufferWriter<byte>();
        await foreach (var item in encryptionResult)
        {
            bufferedResult.Write(item.Span);
        }

        return bufferedResult.WrittenMemory;
    }

    /// <summary>
    /// Encrypts a stream using the Dapr Cryptography encryption functionality.
    /// </summary>
    /// <param name="vaultResourceName">The name of the vault resource used by the operation.</param>
    /// <param name="plaintextStream">The stream containing the bytes of the plaintext value to encrypt.</param>
    /// <param name="keyName">The name of the key to use from the Vault for the encryption operation.</param>
    /// <param name="encryptionOptions">Options informing how the encryption operation should be configured.</param>
    /// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
    /// <returns>An array of encrypted bytes.</returns>
    public override async IAsyncEnumerable<ReadOnlyMemory<byte>> EncryptAsync(
        string vaultResourceName,
        Stream plaintextStream,
        string keyName,
        EncryptionOptions encryptionOptions,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName));
        ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName));
        ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream));
        ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions));
        
        EventHandler<Exception> exceptionHandler = (_, ex) => throw ex;

        var shouldOmitDecryptionKeyName =
            string.IsNullOrWhiteSpace(encryptionOptions
                .DecryptionKeyName); //Whitespace isn't likely a valid key name either

        var encryptRequestOptions = new Autogenerated.EncryptRequestOptions
        {
            ComponentName = vaultResourceName,
            DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(),
            KeyName = keyName,
            KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(),
            OmitDecryptionKeyName = shouldOmitDecryptionKeyName
        };

        if (!shouldOmitDecryptionKeyName)
        {
            ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName,
                nameof(encryptionOptions.DecryptionKeyName));
            encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName;
        }

        var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprEncryptionClient).Assembly,
            this.DaprApiToken, cancellationToken);
        var duplexStream = Client.EncryptAlpha1(grpcCallOptions);

        using var streamProcessor = new EncryptionStreamProcessor();
        try
        {
            streamProcessor.OnException += exceptionHandler;
            await streamProcessor.ProcessStreamAsync(plaintextStream, duplexStream, encryptRequestOptions,
                encryptionOptions.StreamingBlockSizeInBytes,
                cancellationToken);

            await foreach (var value in streamProcessor.GetProcessedDataAsync(cancellationToken))
            {
                yield return value;
            }
        }
        finally
        {
            streamProcessor.OnException -= exceptionHandler;
        }
    }

    /// <summary>
    /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality.
    /// </summary>
    /// <param name="vaultResourceName">The name of the vault resource used by the operation.</param>
    /// <param name="ciphertextBytes">The bytes of the ciphertext value to decrypt.</param>
    /// <param name="keyName">The name of the key to use from the Vault for the decryption operation.</param>
    /// <param name="options">Options informing how the decryption operation should be configured.</param>
    /// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
    /// <returns>An array of decrypted bytes.</returns>
    public override async Task<ReadOnlyMemory<byte>> DecryptAsync(
        string vaultResourceName,
        ReadOnlyMemory<byte> ciphertextBytes,
        string keyName,
        DecryptionOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        using var memoryStream = ciphertextBytes.CreateMemoryStream(true);
        var decryptionResult = DecryptAsync(vaultResourceName, memoryStream, keyName, options, cancellationToken);

        var bufferedResult = new ArrayBufferWriter<byte>();
        await foreach (var item in decryptionResult)
        {
            bufferedResult.Write(item.Span);
        }

        return bufferedResult.WrittenMemory;
    }

    /// <summary>
    /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality.
    /// </summary>
    /// <param name="vaultResourceName">The name of the vault resource used by the operation.</param>
    /// <param name="ciphertextStream">The stream containing the bytes of the ciphertext value to decrypt.</param>
    /// <param name="keyName">The name of the key to use from the Vault for the decryption operation.</param>
    /// <param name="options">Options informing how the decryption operation should be configured.</param>
    /// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
    /// <returns>An asynchronously enumerable array of decrypted bytes.</returns>
    public override async IAsyncEnumerable<ReadOnlyMemory<byte>> DecryptAsync(
        string vaultResourceName,
        Stream ciphertextStream,
        string keyName,
        DecryptionOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName));
        ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName));
        ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream));
        options = options ?? new DecryptionOptions();

        EventHandler<Exception> exceptionHandler = (_, ex) => throw ex;

        var decryptRequestOptions = new Autogenerated.DecryptRequestOptions
        {
            ComponentName = vaultResourceName, 
            KeyName = keyName
        };

        var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprEncryptionClient).Assembly,
            this.DaprApiToken, cancellationToken);
        var duplexStream = Client.DecryptAlpha1(grpcCallOptions);

        using var streamProcessor = new DecryptionStreamProcessor();
        try
        {
            streamProcessor.OnException += exceptionHandler;
            await streamProcessor.ProcessStreamAsync(ciphertextStream, duplexStream, options.StreamingBlockSizeInBytes,
                decryptRequestOptions,
                cancellationToken);

            await foreach (var value in streamProcessor.GetProcessedDataAsync(cancellationToken))
            {
                yield return value;
            }
        }
        finally
        {
            streamProcessor.OnException -= exceptionHandler;
        }
    }
}
